Skip to content

KAFKA-12648: fix bug where thread is re-added to TopologyMetadata when shutting down#11857

Merged
ableegoldman merged 4 commits intoapache:trunkfrom
ableegoldman:12648-HOTFIX-fix-topology-version-listeners
Mar 8, 2022
Merged

KAFKA-12648: fix bug where thread is re-added to TopologyMetadata when shutting down#11857
ableegoldman merged 4 commits intoapache:trunkfrom
ableegoldman:12648-HOTFIX-fix-topology-version-listeners

Conversation

@ableegoldman
Copy link
Member

We used to call TopologyMetadata#maybeNotifyTopologyVersionWaitersAndUpdateThreadsTopologyVersion when a thread was being unregistered/shutting down, to check if any of the futures listening for topology updates had been waiting on this thread and could be completed. Prior to invoking this we make sure to remove the current thread from the TopologyMetadata's threadVersions map, but this thread is actually then re-added in the #maybeNotifyTopologyVersionWaitersAndUpdateThreadsTopologyVersion call.

To fix this, we should break up this method into separate calls for each of its two distinct functions, updating the version and checking for topology update completion. When unregistering a thread, we should only invoke the latter method

@ableegoldman
Copy link
Member Author

cc @wcarlson5

@ableegoldman ableegoldman requested a review from vvcephei March 7, 2022 10:56
Comment on lines -885 to -889
log.info("StreamThread has detected an update to the topology, triggering a rebalance to refresh the assignment");
if (topologyMetadata.isEmpty()) {
mainConsumer.unsubscribe();
}
topologyMetadata.maybeNotifyTopologyVersionWaitersAndUpdateThreadsTopologyVersion(getName());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this has been moved into taskManager#handleTopologyUpdates

*/
void handleTopologyUpdates() {
tasks.maybeCreateTasksFromNewTopologies();
final Set<String> currentNamedTopologies = topologyMetadata.updateThreadTopologyVersion(Thread.currentThread().getName());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't the main fix, but we were playing a little fast and loose with the topology version we were reporting having ack'ed -- tightened this up by first atomically updating the topology version and saving the set of current named topologies, then doing the actual update handling, and then checking the listeners and completing any finished add/remove topology requests

public ReentrantLock topologyLock = new ReentrantLock();
public Condition topologyCV = topologyLock.newCondition();
public List<TopologyVersionWaiters> activeTopologyWaiters = new LinkedList<>();
public List<TopologyVersionListener> activeTopologyUpdateListeners = new LinkedList<>();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just renamed from waiters to listeners

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also another quick question regarding why we need to keep topologyVersion an atomic long? Seems besides the getters all of its updators are under the lock as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find, yeah I believe it no longer needs to be an AtomicLong, I'll change back to long

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right actually no, we. do still need it to be an AtomicLong as we check it in the StreamThread main loop when looking for topology updates. And obviously we don't want to have to grab the full lock for that

final Iterator<TopologyVersionWaiters> iterator = version.activeTopologyWaiters.listIterator();
TopologyVersionWaiters topologyVersionWaiters;
version.topologyLock.lock();
threadVersions.put(threadName, topologyVersion());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wcarlson5 / @guozhangwang / @vvcephei This is the main fix -- we need to split out the version update where we add the current thread with the latest topology version to this threadVersions map, since this of course should only be done when we're reacting to a topology update.

The other function of the method was to check whether we could complete any of the queued listeners, which is why we were invoking this when shutting down a thread. Splitting this out into a separate method avoids ghost threads being left behind in the threadVersions map

}
topologyVersionListener = iterator.next();
final long topologyVersionWaitersVersion = topologyVersionListener.topologyVersion;
if (minThreadVersion >= topologyVersionWaitersVersion) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also refactored this slightly to optimize/clean up this method. It's less about the optimization as we should generally not have too many threads per KafkaStreams runtime, but I found it much easier to follow the logic by computing the minimum version across all threads and then completing all futures listening for the topology to be updated up to that version

Copy link
Contributor

@wcarlson5 wcarlson5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch. LGTM I don't see anything that needs to get into this PR. I appreciate the test fix too

}
topologyVersionListener = iterator.next();
final long topologyVersionWaitersVersion = topologyVersionListener.topologyVersion;
if (minThreadVersion >= topologyVersionWaitersVersion) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we also want to remove the listeners for threads that were removed as well right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The listeners are for the caller threads not stream threads right? I thought since the thread is removed, it would not be counted in the getMinimumThreadVersion() and hence would not block the listeners to be removed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the listeners are for stream threads. They get added in the task manager. Once all threads are at the version the future blocking the calling thread is completed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... just to make sure we are talking about version.activeTopologyUpdateListeners right? These listeners are for the calling thread of the removeNamedTopology / addNamedTopology / start, which would get the wraped futures these listeners are constructed on.

Anyways, my understanding is that when a thread is removed, the getMinimumThreadVersion returned version would not take that removed thread into consideration, so that even the removed thread's version is low it would not block the future being completed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah @guozhangwang yeah the getMinimumThreadVersion should take care of it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think this was already resolved but just to clarify for anyone else reading this/ourselves in the future, yes, the listeners are for the callers of add/removeNamedTopology 👍

Copy link
Contributor

@guozhangwang guozhangwang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ableegoldman the change lgtm overall. But I have a meta question about our synchronization: in the TopologyMetadata class we have two synchronization manners: 1) we use a lock for any changes to the TopologyVersion object, 2) we made the threadVersions a concurrent hashmap, but not all modifications (e.g. register/deregister thread) would require the lock in 1).

That means the TopologyVersion object may not be always consistent from the threadVersions map. Is this okay or intended by our design? If not, maybe we can just make the threadVersions be part of the TopologyVersion and be always updated with the same lock.

}
topologyVersionListener = iterator.next();
final long topologyVersionWaitersVersion = topologyVersionListener.topologyVersion;
if (minThreadVersion >= topologyVersionWaitersVersion) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The listeners are for the caller threads not stream threads right? I thought since the thread is removed, it would not be counted in the getMinimumThreadVersion() and hence would not block the listeners to be removed.

}
topologyVersionListener = iterator.next();
final long topologyVersionWaitersVersion = topologyVersionListener.topologyVersion;
if (minThreadVersion >= topologyVersionWaitersVersion) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... just to make sure we are talking about version.activeTopologyUpdateListeners right? These listeners are for the calling thread of the removeNamedTopology / addNamedTopology / start, which would get the wraped futures these listeners are constructed on.

Anyways, my understanding is that when a thread is removed, the getMinimumThreadVersion returned version would not take that removed thread into consideration, so that even the removed thread's version is low it would not block the future being completed.

@ableegoldman
Copy link
Member Author

All test failures are unrelated, going to merge this now

@ableegoldman ableegoldman merged commit fc7133d into apache:trunk Mar 8, 2022
ableegoldman added a commit to confluentinc/kafka that referenced this pull request Mar 9, 2022
…n shutting down (apache#11857) (#674)

We used to call TopologyMetadata#maybeNotifyTopologyVersionWaitersAndUpdateThreadsTopologyVersion when a thread was being unregistered/shutting down, to check if any of the futures listening for topology updates had been waiting on this thread and could be completed. Prior to invoking this we make sure to remove the current thread from the TopologyMetadata's threadVersions map, but this thread is actually then re-added in the #maybeNotifyTopologyVersionWaitersAndUpdateThreadsTopologyVersion call.

To fix this, we should break up this method into separate calls for each of its two distinct functions, updating the version and checking for topology update completion. When unregistering a thread, we should only invoke the latter method

Reviewers: Guozhang Wang <guozhang@confluent.io>, Walker Carlson <wcarlson@confluent.io>

Co-authored-by: A. Sophie Blee-Goldman <sophie@confluent.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments